﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.InteropServices;
using System.Collections;

namespace Liquifier
{
    public enum EntryType
    {
        Liquified = 0
    }

    /// <summary>
    /// These flags are serialised along with the member and are the only metadata, apart from the name, of the member to be preserved
    /// </summary>
    [Flags]
    public enum SerialisedMemberFlags : byte
    {
        Default = 0,
        DoNotProcessOnSolidify = 1,

    }

    public partial class Liquifer : IDisposable
    {        
        #region Entry Points

        public static LiquidInfo Liquify(object graph, Stream destination, LiquificationContext context = null)
        {
            return Liquify(graph, graph.GetType(), destination, context);
        }

        public static LiquidInfo Liquify(object graph, Type astype, Stream destination, LiquificationContext context = null)
        {
            if (context == null)
            {
                context = new LiquificationContext();

                //The top level object will have to be added explicitly, so liquifier can get the first reference to write
                context.AddReference(graph);
            }

            SerialisationWriter writer = new SerialisationWriter(destination);

            //this object is new, so create a liquifier instance and start writing it to the stream
            using (Liquifer l = new Liquifer(astype, context))
            {
                l.CollectTypeMembers(l.TargetType.Type, true);
                l.CollectMemberValues(graph);

                writer.Write((Int32)EntryType.Liquified);
                writer.Write((Int32)context.GetReference(graph));       //write the unique id for this new reference object

                if (graph is ILiquify)
                {
                    (graph as ILiquify).OnLiquify(l);
                }

                //then serialise its members
                l.Serialise(writer);
            }

            return context.Statistics;
        }

#pragma warning disable 0162

        public static object Solidify(Stream source, SolidificationContext context = null)
        {
            if (context == null)
            {
                context = new SolidificationContext();
            }

            SerialisationReader reader = new SerialisationReader(source);

            EntryType entry = (EntryType)reader.ReadInt32();
            int referenceId = reader.ReadInt32();

            switch (entry)
            {
                case EntryType.Liquified:
                    using (Liquifer l = new Liquifer(context))
                    {
                        l.DeserialiseType(reader);
                        object destination = l.CreateObject(l.TargetType.Type);
                        context.ReferenceItems.Add(referenceId, destination);

                        l.DeserialiseContent(reader);
                                                
                        l.CollectTypeMembers(l.TargetType.Type, false);
                        l.RepatriateMemberValues(destination);

                        if (destination is ISolidify)
                        {
                            (destination as ISolidify).OnSolidified(l);
                        }

                        return destination;
                    }
                    break;
            }

            throw new Exception(string.Format("Unknown Token in Stream: Do not know how to process token: {0}", entry));
      
        }

        protected object CreateObject(Type type)
        {
            var constructor = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(x => x.GetParameters().Length == 0);
            if (constructor != null)
            {
                return constructor.Invoke(null);
            }
            else
            {
                return FormatterServices.GetUninitializedObject(type);
            }
        }

        #endregion

        #region Constructors and Initialisation

        protected Liquifer(Type t, LiquificationContext context)
        {
            this.LiquifyContext = context;
            this.TargetType = TypeData.GetTypeData(t);
        }

        protected Liquifer(SolidificationContext context)
        {
            this.SolidifyContext = context;
        }   

        #endregion

        #region Members

        protected TypeData TargetType;

        protected MemberCollection LocalMembers = new MemberCollection();

        protected LiquificationContext LiquifyContext;
        protected SolidificationContext SolidifyContext;

        protected MemberValueCollection MemberContents = new MemberValueCollection();

        #endregion

        protected static LiquifiableAttribute serializeablesEquivalentSettings = new LiquifiableAttribute(Serialisation.Public | Serialisation.Private, Serialisation.None);

        #region Object, IDisposable Implementation

        public void Dispose()
        {
        }

        public override string ToString()
        {
            return (string.Format("Liquifier [{0}])", TargetType.AssemblyQualifiedName));
        }

        #endregion

        #region Public utlity methods - for use in OnLiquify()/OnSolidify()

        public void AddEntryForSerialisation(object value, string name, SerialisedMemberFlags flags = SerialisedMemberFlags.Default)
        {
            if (MemberContents.FindMemberValue(name) != null)
            {
                throw new Exception(string.Format("Name Already in Use: This Liquifier instance already has a value with Name/ID: {0}",name));
            }

            MemberContents.Add(new MemberValue(name, value, flags));
        }

        public object GetDeserialisedEntry(string name)
        {
            var v = MemberContents.FindMemberValue(name);
            if (v == null)
            {
                throw new Exception(string.Format("Value Not Found: Unable to find member value with ID: {0}",name));
            }
            return v.Value;
        }

        #endregion


    }
}
